/*
 * Copyright 2002-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package carson.web.mvc;

import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * 实现 {@link org.springframework.web.servlet.HandlerMapping}
 * 遵循COC(约定优于配置)原则
 * 将继承自CarsonActionController的类注册到Bean，即可实现简单的URL映射
 * 例如
 * <ul>
 * <li>{@code WelcomeController} -> {@code /welcome}, {@code /welcome/**}</li>
 * <li>{@code CatalogController} -> {@code /catalog}, {@code /catalog/**}</li>
 * </ul>
 * 例如：请求url{@code /welcome/hello} 对应调用 WelcomeController的hello方法
 *
 * @author Carson Chen
 */
public class CarsonControllerNameHandlerMapping extends AbstractDetectingUrlHandlerMapping {

    public CarsonControllerNameHandlerMapping() {
        super();
        //URL对大小写不敏感
        setPathMatcher(new CaseInsenseticePathMatcher());
    }

    /**
     * 控制器的类后缀
     */
    private static final String CONTROLLER_SUFFIX = "Controller";

    private String pathPrefix;

    private String basePackage;

    /**
     * 设置路径的前缀.
     * <p>Default is a plain slash ("/"). A path like "/mymodule" can be specified
     * in order to have controller path mappings prefixed with that path, e.g.
     * "/mymodule/buyform" instead of "/buyform" for the class name "BuyForm".
     */
    public void setPathPrefix(String prefixPath) {
        this.pathPrefix = prefixPath;
        if (StringUtils.hasLength(this.pathPrefix)) {
            if (!this.pathPrefix.startsWith("/")) {
                this.pathPrefix = "/" + this.pathPrefix;
            }
            if (this.pathPrefix.endsWith("/")) {
                this.pathPrefix = this.pathPrefix.substring(0, this.pathPrefix.length() - 1);
            }
        }
    }

    /**
     * Set the base package to be used for generating path mappings,
     * including all subpackages underneath this packages as path elements.
     * <p>Default is {@code null}, using the short class name for the
     * generated path, with the controller's package not represented in the path.
     * Specify a base package like "com.mycompany.myapp" to include subpackages
     * within that base package as path elements, e.g. generating the path
     * "/mymodule/buyform" for the class name "com.mycompany.myapp.mymodule.BuyForm".
     * Subpackage hierarchies are represented as individual path elements,
     * e.g. "/mymodule/mysubmodule/buyform" for the class name
     * "com.mycompany.myapp.mymodule.mysubmodule.BuyForm".
     */
    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
        if (StringUtils.hasLength(this.basePackage) && !this.basePackage.endsWith(".")) {
            this.basePackage = this.basePackage + ".";
        }
    }


    protected String[] buildUrlsForHandler(String beanName, Class<?> beanClass) {
        return generatePathMappings(beanClass);
    }

    /**
     * Generate the actual URL paths for the given controller class.
     * <p>Subclasses may choose to customize the paths that are generated
     * by overriding this method.
     *
     * @param beanClass the controller bean class to generate a mapping for
     * @return the URL path mappings for the given controller
     */
    protected String[] generatePathMappings(Class<?> beanClass) {
        StringBuilder pathMapping = buildPathPrefix(beanClass);
        String className = ClassUtils.getShortName(beanClass);
        String path = (className.endsWith(CONTROLLER_SUFFIX) ?
                className.substring(0, className.lastIndexOf(CONTROLLER_SUFFIX)) : className);
        if (path.length() > 0) {
            pathMapping.append(path);
        }
        return new String[]{pathMapping.toString(), pathMapping.toString() + "/**"};
    }

    /**
     * Build a path prefix for the given controller bean class.
     *
     * @param beanClass the controller bean class to generate a mapping for
     * @return the path prefix, potentially including subpackage names as path elements
     */
    private StringBuilder buildPathPrefix(Class<?> beanClass) {
        StringBuilder pathMapping = new StringBuilder();
        if (this.pathPrefix != null) {
            pathMapping.append(this.pathPrefix);
            pathMapping.append("/");
        } else {
            pathMapping.append("/");
        }
        if (this.basePackage != null) {
            String packageName = ClassUtils.getPackageName(beanClass);
            if (packageName.startsWith(this.basePackage)) {
                String subPackage = packageName.substring(this.basePackage.length()).replace('.', '/');
                pathMapping.append(subPackage);
                pathMapping.append("/");
            }
        }
        return pathMapping;
    }


    private Set<String> excludedPackages = Collections.singleton("org.springframework.web.servlet.mvc");

    private Set<Class<?>> excludedClasses = Collections.emptySet();


    /**
     * 设置排除掉的包，被排除掉的包里的类将不会被添加到路径映射
     */
    public void setExcludedPackages(String... excludedPackages) {
        this.excludedPackages = (excludedPackages != null) ?
                new HashSet<String>(Arrays.asList(excludedPackages)) : new HashSet<String>();
    }

    /**
     * 设置排除掉的类，将不会被添加到路径映射
     */
    public void setExcludedClasses(Class<?>... excludedClasses) {
        this.excludedClasses = (excludedClasses != null) ?
                new HashSet<Class<?>>(Arrays.asList(excludedClasses)) : new HashSet<Class<?>>();
    }


    /**
     * This implementation delegates to {@link #buildUrlsForHandler},
     * 实现将Controller类映射到Url
     */
    @Override
    protected String[] determineUrlsForHandler(String beanName) {
        Class<?> beanClass = getApplicationContext().getType(beanName);
        if (isEligibleForMapping(beanName, beanClass)) {
            return buildUrlsForHandler(beanName, beanClass);
        } else {
            return null;
        }
    }

    /**
     * 判断是否是需要添加到映射的Controller
     *
     * @param beanName  the name of the controller bean
     * @param beanClass the concrete class of the controller bean
     * @return whether the specified class is excluded
     * @see #setExcludedPackages
     * @see #setExcludedClasses
     */
    protected boolean isEligibleForMapping(String beanName, Class<?> beanClass) {
        if (beanClass == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " +
                        "because its bean type could not be determined");
            }
            return false;
        }
        if (this.excludedClasses.contains(beanClass)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " +
                        "because its bean class is explicitly excluded: " + beanClass.getName());
            }
            return false;
        }
        String beanClassName = beanClass.getName();
        for (String packageName : this.excludedPackages) {
            if (beanClassName.startsWith(packageName)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Excluding controller bean '" + beanName + "' from class name mapping " +
                            "because its bean class is defined in an excluded package: " + beanClass.getName());
                }
                return false;
            }
        }
        return isControllerType(beanClass);
    }

    /**
     * 判断是否是Controller类型
     *
     * @param beanClass the class to introspect
     */
    protected boolean isControllerType(Class<?> beanClass) {
        return CarsonActionController.class.isAssignableFrom(beanClass);
    }


}
